{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "77d79657",
   "metadata": {},
   "source": [
    "\n",
    "# NVIDIA NIMs with Tool Calling for Agents"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8fdde48",
   "metadata": {
    "jp-MarkdownHeadingCollapsed": true
   },
   "source": [
    "This notebook will use a [NVIDIA Llama 3.1 NIM](https://developer.nvidia.com/blog/supercharging-llama-3-1-across-nvidia-platforms/) with tool-calling agent capabilities in generative AI solutions. As mentioned in this [Introductory Blog on LLM Agents](https://developer.nvidia.com/blog/introduction-to-llm-agents/), agents can be described as AI systems that use LLMs to reason through a problem, create a plan to solve the problem, execute the plan with the help of a set of tools, and use memory to store meaningful context of the system state. \n",
    "\n",
    "The notebook is designed to provide an intro to merely one of the capabilities of agent systems: **tool calling**. \n",
    "\n",
    "**Tools** are interfaces that accept input, execute an action, and then return a result of that action in a structured output according to a pre-defined schema. They often encompass external API calls that the agent can use to perform tasks that go beyond the capabilities of the LLM, but do not have to be external API calls. For example, to get the current weather in San Diego, a weather tool might be used. Or to get the current score of the 49ers game, a generic web search tool or ESPN tool might be used. \n",
    "\n",
    "## What is NVIDIA NIM and How do They Support Tool Calling for Agents?\n",
    "### What is NIM?\n",
    "NIM supports models across domains like chat, embedding, and re-ranking models \n",
    "from the community as well as NVIDIA. These models are optimized by NVIDIA to deliver the best performance on NVIDIA \n",
    "accelerated infrastructure and deployed as a NIM, an easy-to-use, prebuilt containers that deploy anywhere using a single \n",
    "command on NVIDIA accelerated infrastructure. If you're new to NIMs with LangChain, check out the [documentation](https://python.langchain.com/docs/integrations/providers/nvidia/).\n",
    "\n",
    "Now, NIMs support tool calling, also known as \"function calling\" for models that have the aforementioned capability. \n",
    "\n",
    "This notebook will demonstrate a model that supports function calling, [Llama 3.1 8b-instruct](https://build.nvidia.com/meta/llama-3_1-8b-instruct). \n",
    "\n",
    "### What does it mean for NIM to support tool usage?\n",
    "In order to support tool usage in an agent workflow, first an LLM must be trained to detect when a function should be called and output a structured response like JSON that contains the function to be called and its arguments. \n",
    "\n",
    "Next, the model is packaged as a NIM, meaning it's optimized to deliver best performance on NVIDIA accelerated infrastructure and easy to deploy as well as use. This microservice packaging also uses OpenAI compatible APIs, so developers can build world-class generative AI agents with ease.\n",
    "\n",
    "Let's see how to use tools for agentic applications with LangGraph. \n",
    "\n",
    "*Note: lots of the educational content is adapted from https://langchain-ai.github.io/langgraph/concepts/high_level/.*"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "120455e4",
   "metadata": {},
   "source": [
    "##  🔨 Tool Usage -- Web Search\n",
    "\n",
    "Since a LLM does not have access to the most up-to-date information on the Internet, [Tavily Search](https://docs.tavily.com/docs/tavily-api/introduction) acts as a tool to provide a generative AI application with real-time online information.  Tavily is a search engmine that is optimized for AI developers and AI agents. A singular API call abstracts searching, scraping, filtering, and extracting relevant information from online sources. \n",
    "\n",
    "We'll enhance our NIM, [Llama 3.1-8b-instruct](https://build.nvidia.com/meta/llama-3_1-8b-instruct), with Tavily search. "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b8f8b6f",
   "metadata": {},
   "source": [
    "Install pre-requesites. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fe4ec61f",
   "metadata": {},
   "outputs": [],
   "source": [
    "%pip install -U langchain langgraph langchain-nvidia-ai-endpoints langchain-community langchain-openai tavily-python geocoder"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6c65b376",
   "metadata": {},
   "source": [
    "If you're using NVIDIA hosted NIMs, you'll need to use an API key which you can setup below. Follow [NVIDIA NIMs LangChain documentation](https://python.langchain.com/docs/integrations/chat/nvidia_ai_endpoints/) for more information on accessing and using NIMs. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "aaeb35a9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "if \"NVIDIA_API_KEY\" not in os.environ:\n",
    "    os.environ[\"NVIDIA_API_KEY\"] = getpass.getpass(\"Enter your NVIDIA API key\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e190dc5e",
   "metadata": {},
   "source": [
    "Declare your model that supports tool calling. In this example, we use [Llama 3.1-8b-instruct](https://build.nvidia.com/meta/llama-3_1-8b-instruct). "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "579881ca",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_nvidia_ai_endpoints import ChatNVIDIA\n",
    "\n",
    "llm = ChatNVIDIA(model=\"meta/llama-3.1-70b-instruct\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9ce17567",
   "metadata": {},
   "source": [
    "Initialize [Tavily Tool](https://python.langchain.com/docs/integrations/tools/tavily_search/)\n",
    "\n",
    "Note that this requires an API key - they have a free tier, but if you don't have one or don't want to create one, you can always ignore this step or use a different tool. \n",
    "\n",
    "Once you create your API key, you will need to set it in the environment."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "c8832545-d3c1-404f-afdb-6a00891f84c9",
   "metadata": {},
   "outputs": [],
   "source": [
    "import getpass\n",
    "import os\n",
    "\n",
    "if \"TAVILY_API_KEY\" not in os.environ:\n",
    "    os.environ[\"TAVILY_API_KEY\"] = getpass.getpass(\"Enter your Tavily API key\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "e1d1511d",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_community.tools.tavily_search import TavilySearchResults\n",
    "\n",
    "# Declare a single tool, Tavily search\n",
    "tools = [TavilySearchResults(max_results=1)]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f63dd76-d8c7-429e-bf7c-d2f575ef8340",
   "metadata": {},
   "source": [
    "We will wrap the tools as a `ToolNode` which will be beneficial to use in LangGraph later."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "75437d15-2e38-4673-850c-3272274aa917",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.prebuilt import ToolNode\n",
    "\n",
    "tool_node = ToolNode(tools)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d69d35aa-09d6-4484-a230-c0fe4c2b6bcb",
   "metadata": {},
   "source": [
    "Let's invoke the tool manually to see the result."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "a433c5c5-c69e-410c-bfbd-df9b3f9bcf3b",
   "metadata": {
    "scrolled": true
   },
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'messages': [ToolMessage(content='[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.775, \\'lon\\': -122.4183, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1733286727, \\'localtime\\': \\'2024-12-03 20:32\\'}, \\'current\\': {\\'last_updated_epoch\\': 1733286600, \\'last_updated\\': \\'2024-12-03 20:30\\', \\'temp_c\\': 14.4, \\'temp_f\\': 57.9, \\'is_day\\': 0, \\'condition\\': {\\'text\\': \\'Clear\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/night/113.png\\', \\'code\\': 1000}, \\'wind_mph\\': 3.4, \\'wind_kph\\': 5.4, \\'wind_degree\\': 355, \\'wind_dir\\': \\'N\\', \\'pressure_mb\\': 1021.0, \\'pressure_in\\': 30.15, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 60, \\'cloud\\': 0, \\'feelslike_c\\': 14.7, \\'feelslike_f\\': 58.4, \\'windchill_c\\': 13.2, \\'windchill_f\\': 55.8, \\'heatindex_c\\': 13.0, \\'heatindex_f\\': 55.4, \\'dewpoint_c\\': 9.9, \\'dewpoint_f\\': 49.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 0.0, \\'gust_mph\\': 7.0, \\'gust_kph\\': 11.3}}\"}]', name='tavily_search_results_json', tool_call_id='tool_call_id', artifact={'query': \"What's the weather in San Francisco?\", 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'title': 'Weather in San Francisco', 'url': 'https://www.weatherapi.com/', 'content': \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.775, 'lon': -122.4183, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1733286727, 'localtime': '2024-12-03 20:32'}, 'current': {'last_updated_epoch': 1733286600, 'last_updated': '2024-12-03 20:30', 'temp_c': 14.4, 'temp_f': 57.9, 'is_day': 0, 'condition': {'text': 'Clear', 'icon': '//cdn.weatherapi.com/weather/64x64/night/113.png', 'code': 1000}, 'wind_mph': 3.4, 'wind_kph': 5.4, 'wind_degree': 355, 'wind_dir': 'N', 'pressure_mb': 1021.0, 'pressure_in': 30.15, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 60, 'cloud': 0, 'feelslike_c': 14.7, 'feelslike_f': 58.4, 'windchill_c': 13.2, 'windchill_f': 55.8, 'heatindex_c': 13.0, 'heatindex_f': 55.4, 'dewpoint_c': 9.9, 'dewpoint_f': 49.8, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 0.0, 'gust_mph': 7.0, 'gust_kph': 11.3}}\", 'score': 0.9999001, 'raw_content': None}], 'response_time': 2.09})]}"
      ]
     },
     "execution_count": 4,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from langchain_core.messages import AIMessage\n",
    "\n",
    "message_with_single_tool_call = AIMessage(\n",
    "    content=\"\",\n",
    "    tool_calls=[\n",
    "        {\n",
    "            \"name\": \"tavily_search_results_json\",\n",
    "            \"args\": {\"query\": \"What's the weather in San Francisco?\"},\n",
    "            \"id\": \"tool_call_id\",\n",
    "            \"type\": \"tool_call\",\n",
    "        }\n",
    "    ],\n",
    ")\n",
    "\n",
    "tool_node.invoke({\"messages\": [message_with_single_tool_call]})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6ae74552-a8d9-4a05-ab55-47d6b3cfea5d",
   "metadata": {},
   "source": [
    "Now, let's see how to use the tool with a chat model. This requires binding the tool to the LLM. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "771400bb-3a7d-4c87-b7fe-30e2f9c92f5a",
   "metadata": {},
   "outputs": [],
   "source": [
    "llm_with_tools = llm.bind_tools(tools)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "5a15de17-dc4b-467a-bbf6-7526b2adb069",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[{'name': 'tavily_search_results_json',\n",
       "  'args': {'query': 'San Francisco weather'},\n",
       "  'id': 'chatcmpl-tool-56daf0e116464eb0ac656b7cb2552d2b',\n",
       "  'type': 'tool_call'}]"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "llm_with_tools.invoke(\"What's the weather in San Francisco?\").tool_calls"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef07259b-2baf-4407-a3b3-3e48d060adfa",
   "metadata": {},
   "source": [
    "As you can see, the LLM decides that it is best to use the `tavily_search_results_json` tool and that the query is \"San Francisco Weather today\". Output is structured accordingly."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6909a295-e9d4-416f-a57d-c08a2bc5f1c5",
   "metadata": {},
   "source": [
    "Let's send this as a message to the ToolNode -- more on this in the next section :) "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "335a3176-2f52-4001-afe7-d6536498493c",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "{'messages': [ToolMessage(content='[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{\\'location\\': {\\'name\\': \\'San Francisco\\', \\'region\\': \\'California\\', \\'country\\': \\'United States of America\\', \\'lat\\': 37.775, \\'lon\\': -122.4183, \\'tz_id\\': \\'America/Los_Angeles\\', \\'localtime_epoch\\': 1733286727, \\'localtime\\': \\'2024-12-03 20:32\\'}, \\'current\\': {\\'last_updated_epoch\\': 1733286600, \\'last_updated\\': \\'2024-12-03 20:30\\', \\'temp_c\\': 14.4, \\'temp_f\\': 57.9, \\'is_day\\': 0, \\'condition\\': {\\'text\\': \\'Clear\\', \\'icon\\': \\'//cdn.weatherapi.com/weather/64x64/night/113.png\\', \\'code\\': 1000}, \\'wind_mph\\': 3.4, \\'wind_kph\\': 5.4, \\'wind_degree\\': 355, \\'wind_dir\\': \\'N\\', \\'pressure_mb\\': 1021.0, \\'pressure_in\\': 30.15, \\'precip_mm\\': 0.0, \\'precip_in\\': 0.0, \\'humidity\\': 60, \\'cloud\\': 0, \\'feelslike_c\\': 14.7, \\'feelslike_f\\': 58.4, \\'windchill_c\\': 13.2, \\'windchill_f\\': 55.8, \\'heatindex_c\\': 13.0, \\'heatindex_f\\': 55.4, \\'dewpoint_c\\': 9.9, \\'dewpoint_f\\': 49.8, \\'vis_km\\': 16.0, \\'vis_miles\\': 9.0, \\'uv\\': 0.0, \\'gust_mph\\': 7.0, \\'gust_kph\\': 11.3}}\"}]', name='tavily_search_results_json', tool_call_id='chatcmpl-tool-8bc0c2286f9f45af9d5383e7d74f3fe1', artifact={'query': 'San Francisco weather', 'follow_up_questions': None, 'answer': None, 'images': [], 'results': [{'title': 'Weather in San Francisco', 'url': 'https://www.weatherapi.com/', 'content': \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.775, 'lon': -122.4183, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1733286727, 'localtime': '2024-12-03 20:32'}, 'current': {'last_updated_epoch': 1733286600, 'last_updated': '2024-12-03 20:30', 'temp_c': 14.4, 'temp_f': 57.9, 'is_day': 0, 'condition': {'text': 'Clear', 'icon': '//cdn.weatherapi.com/weather/64x64/night/113.png', 'code': 1000}, 'wind_mph': 3.4, 'wind_kph': 5.4, 'wind_degree': 355, 'wind_dir': 'N', 'pressure_mb': 1021.0, 'pressure_in': 30.15, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 60, 'cloud': 0, 'feelslike_c': 14.7, 'feelslike_f': 58.4, 'windchill_c': 13.2, 'windchill_f': 55.8, 'heatindex_c': 13.0, 'heatindex_f': 55.4, 'dewpoint_c': 9.9, 'dewpoint_f': 49.8, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 0.0, 'gust_mph': 7.0, 'gust_kph': 11.3}}\", 'score': 0.9909054, 'raw_content': None}], 'response_time': 2.45})]}"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "tool_node.invoke({\"messages\": [llm_with_tools.invoke(\"What's the weather in San Francisco?\")]})"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b5e9bbb9",
   "metadata": {},
   "source": [
    "## 🔨 Tool Usage -- Adding on a Custom Tool and Using LangGraph\n",
    "\n",
    "Let's see how to [define a custom tool](https://python.langchain.com/docs/how_to/custom_tools/) for your NIM agent and how it handles multiple tools.  \n",
    "\n",
    "We'll enhance the NIM with Tavily search with some custom tools to determine a user's current location (based on IP address) and return a latitude and longitude. We will use these tools to have Tavily look up the weather in the user's current location.\n",
    "\n",
    "In addition, we'll see how to use the `ToolNode` we declared earlier in a graph declared with LangGraph. We'll use an agent that repeatedly calls an LLM deciding which tools to call, the input to those tools, executes/produces output, and then feeds the outputs back to the LLM as observation. When no more tools are needed, the loop ends. "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "46052285-7331-44c2-a7dc-34ebbe4d6b8c",
   "metadata": {},
   "source": [
    "First, let's create a custom tool to determine a user's location based off IP address. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "e9d8ed5f-b6e9-495f-85ff-e431d39475c4",
   "metadata": {},
   "outputs": [],
   "source": [
    "import geocoder\n",
    "from langchain.tools import tool\n",
    "from typing import Tuple\n",
    "\n",
    "@tool\n",
    "def get_current_location() -> list:\n",
    "    \"\"\"Return the current location of the user based on IP address\"\"\"\n",
    "    loc = geocoder.ip('me')\n",
    "    return loc.latlng    "
   ]
  },
  {
   "cell_type": "markdown",
   "id": "089e3223-50f3-4e8e-9043-24c792ca7daf",
   "metadata": {},
   "source": [
    "Let's update the tools and the `ToolNode` to use the Tavily tool delcared earlier and also add the `get_current_location` tool."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "b71d7d05-d3ec-4005-911c-3e44df8102b4",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Declare two tools: Tavily and custom get_current_location tool.\n",
    "tools = [TavilySearchResults(max_results=1), get_current_location]\n",
    "tool_node = ToolNode(tools)\n",
    "\n",
    "# be sure to bind the updated tools to the LLM!\n",
    "llm_with_tools = llm.bind_tools(tools)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e0a6ed4-d6a8-4e0b-a094-40adf98f77d4",
   "metadata": {},
   "source": [
    "Let's create a graph! LangGraph models agent workflows as graphs and the behavior of the agent is defined by 3 key pieces:\n",
    "1) `State`: shared data structure that represents the snapshot of the application. In this example, the state consists of messages.\n",
    "2) `Nodes`: Python functions that encode the logic of the agents. They receive the state as input and then perform some actions and return an updated State. In this example, the nodes are an agent and tools. \n",
    "3) `Edges`: Python functions that determine which Node to execute next based on the State.\n",
    "\n",
    "A `StateGraph` is the main graph class used and is parameterized to use `MessagesState` as the graph state."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "9dc8754b-0734-4eec-98e3-0234d3c111f3",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal\n",
    "\n",
    "from langgraph.graph import StateGraph, MessagesState\n",
    "\n",
    "# in this graph continue until no more tools\n",
    "def should_continue(state: MessagesState) -> Literal[\"tools\", \"__end__\"]:\n",
    "    messages = state[\"messages\"]\n",
    "    last_message = messages[-1]\n",
    "    if last_message.tool_calls:\n",
    "        return \"tools\"\n",
    "    return \"__end__\"\n",
    "\n",
    "# call the model on the current messages\n",
    "def call_model(state: MessagesState):\n",
    "    messages = state[\"messages\"]\n",
    "    response = llm_with_tools.invoke(messages)\n",
    "    return {\"messages\": [response]}\n",
    "\n",
    "\n",
    "workflow = StateGraph(MessagesState)\n",
    "\n",
    "# Define the two nodes we will cycle between\n",
    "workflow.add_node(\"agent\", call_model)\n",
    "workflow.add_node(\"tools\", tool_node)\n",
    "\n",
    "# Define edges of the graph\n",
    "workflow.add_edge(\"__start__\", \"agent\")\n",
    "workflow.add_conditional_edges(\n",
    "    \"agent\",\n",
    "    should_continue,\n",
    ")\n",
    "\n",
    "\n",
    "workflow.add_edge(\"tools\", \"agent\")\n",
    "\n",
    "# check structure of graph by compiling it\n",
    "app = workflow.compile()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f84c68ce-bb8d-4e95-94fa-fbb9c40c01e5",
   "metadata": {},
   "source": [
    "Let's see a visual representation of the graph. As you can see, the agent will keep calling tools until it's finished."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "0deb52d9-51a9-4d90-88e0-402d7b77e6e7",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<IPython.core.display.Image object>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "from IPython.display import Image, display\n",
    "\n",
    "try:\n",
    "    display(Image(app.get_graph().draw_mermaid_png()))\n",
    "except Exception:\n",
    "    # This requires some extra dependencies and is optional\n",
    "    pass"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3365e44f-cbb2-4822-a910-05d8aeaf4982",
   "metadata": {},
   "source": [
    "And now let's run the graph in 2 examples! First, we'll try a query that only requires one tool call. Then we'll try a query that requires multiple tool calls."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "1d4ca572-4016-4ab6-8791-ffaa791d86ac",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What's the weather in San Francisco?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  tavily_search_results_json (chatcmpl-tool-3229d85fd4a94457b1b7adab2473341a)\n",
      " Call ID: chatcmpl-tool-3229d85fd4a94457b1b7adab2473341a\n",
      "  Args:\n",
      "    query: San Francisco weather\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: tavily_search_results_json\n",
      "\n",
      "[{\"url\": \"https://www.weatherapi.com/\", \"content\": \"{'location': {'name': 'San Francisco', 'region': 'California', 'country': 'United States of America', 'lat': 37.775, 'lon': -122.4183, 'tz_id': 'America/Los_Angeles', 'localtime_epoch': 1733288183, 'localtime': '2024-12-03 20:56'}, 'current': {'last_updated_epoch': 1733287500, 'last_updated': '2024-12-03 20:45', 'temp_c': 14.4, 'temp_f': 57.9, 'is_day': 0, 'condition': {'text': 'Clear', 'icon': '//cdn.weatherapi.com/weather/64x64/night/113.png', 'code': 1000}, 'wind_mph': 3.4, 'wind_kph': 5.4, 'wind_degree': 355, 'wind_dir': 'N', 'pressure_mb': 1021.0, 'pressure_in': 30.15, 'precip_mm': 0.0, 'precip_in': 0.0, 'humidity': 60, 'cloud': 0, 'feelslike_c': 14.7, 'feelslike_f': 58.4, 'windchill_c': 13.2, 'windchill_f': 55.8, 'heatindex_c': 13.0, 'heatindex_f': 55.4, 'dewpoint_c': 9.9, 'dewpoint_f': 49.8, 'vis_km': 16.0, 'vis_miles': 9.0, 'uv': 0.0, 'gust_mph': 7.0, 'gust_kph': 11.3}}\"}]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "The current weather in San Francisco is clear with a temperature of 14.4°C (57.9°F) and a wind speed of 3.4 mph (5.4 kph).\n"
     ]
    }
   ],
   "source": [
    "# example with a single tool call\n",
    "for chunk in app.stream(\n",
    "    {\"messages\": [(\"human\", \"What's the weather in San Francisco?\")]}, stream_mode=\"values\"\n",
    "):\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "6c297224-9c76-4a3c-a085-61546239dab8",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "================================\u001b[1m Human Message \u001b[0m=================================\n",
      "\n",
      "What's the weather where I currently am?\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "Tool Calls:\n",
      "  get_current_location (chatcmpl-tool-06bd7359d7ba4b35a6a1bc17978f3a40)\n",
      " Call ID: chatcmpl-tool-06bd7359d7ba4b35a6a1bc17978f3a40\n",
      "  Args:\n",
      "=================================\u001b[1m Tool Message \u001b[0m=================================\n",
      "Name: get_current_location\n",
      "\n",
      "[37.6688, -122.0808]\n",
      "==================================\u001b[1m Ai Message \u001b[0m==================================\n",
      "\n",
      "<|python_tag|>{\"current_weather\": \"Mostly Cloudy\",\"temperature\": \"58\",\"feels_like\": \"57\",\"min_temp\": \"47\",\"max_temp\": \"63\",\"humidity\": \"83\",\"wind_speed\": \"20.75\",\"cloudiness\": \"Overcast clouds\",\"sunrise\": \"06:34\",\"sunset\": \"16:54\",\"location\": \"San Francisco, USA\"}\n"
     ]
    }
   ],
   "source": [
    "# example with a multiple tool calls\n",
    "for chunk in app.stream(\n",
    "    {\"messages\": [(\"human\", \"What's the weather where I currently am?\")]}, stream_mode=\"values\"\n",
    "):\n",
    "    chunk[\"messages\"][-1].pretty_print()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cd04f130-3f9b-4a0d-a018-d954dc41ad4b",
   "metadata": {},
   "source": [
    "We already declared our LLM, so we don't need to redeclare it. However, we do want to update the agent to have the updated tools."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "42ce0ec8-d5bb-4ba8-b2d6-6fe3a0c0aeec",
   "metadata": {},
   "source": [
    "## Conclusion\n",
    "You've now seen how to use NIMs to do tool calling, an important capability of agents. As mentioned earlier, tools are just one part of agent capabilities, so check out other notebook so see how tools can be used with othe techniques to create agent workflows.\n",
    "\n",
    "If you're ready to explore more complicated agent workflows, check out [this blog](https://developer.nvidia.com/blog/build-an-agentic-rag-pipeline-with-llama-3-1-and-nvidia-nemo-retriever-nims/) on how to improve your RAG pipeline with agents with Llama 3.1 and NVIDIA NemMo Retriever NIMs."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": ".venv",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.4"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
